using System;
using System.Net;
using System.Net.Sockets;
using Lypshare.Net.Client.Packets;
using System.Threading.Tasks;
using System.Timers;

namespace Lypshare.Net.Client
{
	/// <summary>
	/// Connection.
	/// </summary>
	/// <exception cref='ArgumentNullException'>
	/// Is thrown when an argument passed to a method is invalid because it is <see langword="null" /> .
	/// </exception>
    public class Connection : IConnection
    {
        // --------------------------------------------------------
        // Class Events
        // --------------------------------------------------------


        /// <summary>
        /// Occurs when data received.
        /// </summary>
        public event Action<IConnection, DataEventArgs> DataReceived = null;


        /// <summary>
        /// Occurs when data sent.
        /// </summary>
        public event Action<IConnection, DataEventArgs> DataSent = null;


        /// <summary>
        /// Occurs when closed.
        /// </summary>
        public event Action<IConnection> Closed = null;


        // --------------------------------------------------------
        // Class Variables
        // --------------------------------------------------------
		
		
		/// <summary>
        /// The _client identifier.
        /// </summary>
        private int _clientId = 0;
		

        /// <summary>
        /// The _session.
        /// </summary>
        private INetworkSession _session = null;
		
		
		/// <summary>
		/// The _listener.
		/// </summary>
		private UdpClient _listener = null;
		
		
		/// <summary>
		/// The _endpoint.
		/// </summary>
		private IPEndPoint _endpoint = null;
		
		
		/// <summary>
		/// The _address.
		/// </summary>
		private IPAddress _address = null;
		
		
		/// <summary>
		/// The _local network.
		/// </summary>
		private bool _localNetwork = false;
		
		
		/// <summary>
		/// The _synchronized.
		/// </summary>
		private bool _synchronized = false;
		
		
		/// <summary>
		/// The _connected.
		/// </summary>
		private bool _connected = true;
		
		
		/// <summary>
		/// The _timeout.
		/// </summary>
		private Timer _timeout = null;


        // --------------------------------------------------------
        // Class Properties
        // --------------------------------------------------------
		
		
		/// <summary>
		/// Gets the client identifier.
		/// </summary>
		/// <value>
		/// The client identifier.
		/// </value>
		public int ClientId 
        {
            get { return _clientId; }

        } // Property: ClientId
		
		
        /// <summary>
        /// Gets the session.
        /// </summary>
        /// <value>
        /// The session.
        /// </value>
        public INetworkSession Session
        {
            get { return _session; }

        } // Property: Session


        /// <summary>
        /// Gets the scope.
        /// </summary>
        /// <value>
        /// The scope.
        /// </value>
        public String Scope
        {
            get { return _session.Scope; }

        } // Property: Scope
		
		
		/// <summary>
		/// Gets a value indicating whether this instance is local network.
		/// </summary>
		/// <value>
		/// <c>true</c> if this instance is local network; otherwise, <c>false</c>.
		/// </value>
		public bool IsLocalNetwork
		{
			get { return _localNetwork; }
			
		} // Property: IsLocalNetwork
		
		
		/// <summary>
		/// Gets a value indicating whether this instance is synchronized.
		/// </summary>
		/// <value>
		/// <c>true</c> if this instance is synchronized; otherwise, <c>false</c>.
		/// </value>
		public bool IsSynchronized
		{
			get { return _synchronized; }
			
		} // Property: IsSynchronied
		
		
		/// <summary>
		/// Gets a value indicating whether this instance is connected.
		/// </summary>
		/// <value>
		/// <c>true</c> if this instance is connected; otherwise, <c>false</c>.
		/// </value>
		public bool IsConnected
		{
			get { return _connected; }
			
		} // Proeprty: IsConnected


        /// <summary>
        /// Gets or sets the tag.
        /// </summary>
        /// <value>
        /// The tag.
        /// </value>
        public IConnectionTag Tag
        {
            get;
            set;

        } // Property: Tag


        // --------------------------------------------------------
        // Class Functions
        // --------------------------------------------------------


        /// <summary>
        /// Initializes a new instance of the <see cref="Lypshare.Net.Client.Connection"/> class.
        /// </summary>
        /// <param name='session'>
        /// Session.
        /// </param>
        /// <param name='clientId'>
        /// Client identifier.
        /// </param>
        /// <param name='address'>
        /// Address.
        /// </param>
        /// <exception cref='ArgumentNullException'>
        /// Is thrown when an argument passed to a method is invalid because it is <see langword="null" /> .
        /// </exception>
        public Connection(INetworkSession session, int clientId, IPAddress address)
        {
            if (session == null) throw new ArgumentNullException("session");
            if (address == null) throw new ArgumentNullException("address");

            _session = session;
            _clientId = clientId;
			_address = address;
			
			_timeout = new Timer(4000);
			
			_timeout.Elapsed += 
				OnTimeoutElapsed;
			
			// Link disconnect
			session.ClientLeft += 
				OnSessionClientLeft;
			
        } // Constructor
		
		
		/// <summary>
		/// Listen this instance.
		/// </summary>
        private int Listen()
        {
			lock(_timeout)
			{
				if(_listener == null)
				{
		            try
		            {
		                _listener = new UdpClient(
		                    new IPEndPoint(IPAddress.Any, 0));
		
		                // Setup our listener:
		                // DontFragment allows Internet Protocol (IP) 
		                // datagrams to be fragmented
		                _listener.DontFragment = true;
						
						// Begin receive the packets
						_listener.BeginReceive(
							ReceiveCallback, null);
		
		            } // try
		            catch (Exception ex)
		            {
						Console.WriteLine(ex.Message);
						
		            } // catch (Exception)
					
					// Reset timeout
					_timeout.Stop();
					_timeout.Start();
					
				} // if(_listener == null)
				
				// Return current port
				return ((IPEndPoint)_listener
					.Client.LocalEndPoint).Port;
				
			} // lock(_timeout)
        } // Listen
		
		
		/// <summary>
		/// Synchronize this instance.
		/// </summary>
		private void Synchronize()
		{
			lock(this)
			{
				while(true)
				{
					if(IsSynchronized)
						return;
					
					// Load remote client port
					var remotePort = Session.Synchronize(
						this, Listen());
					
					if(_address.Equals(Session.Address))
					{
						var endpoint = new IPEndPoint(
							IPAddress.Broadcast, remotePort);
						
						var buffer = new PacketBuffer(
							new BroadcastPacket(ClientId));
						
						for(int i = 0; i < 10; i++)
							_listener.Send(buffer.Data, 
			               		(int)buffer.Length, endpoint);
						
					} // if(_address.Equals(Session.Address))
					else
					{
						var endpoint = new IPEndPoint(
							_address, remotePort);
						
						var buffer = new PacketBuffer(
							new PunchingPacket());
						
						for(int i = 0; i < 10; i++)
							_listener.Send(buffer.Data, 
			               		(int)buffer.Length, endpoint);
						
					} // else
					
					System.Threading
						.Thread.Sleep(1000);
					
				} // while(true)
			} // lock(this)
		} // Synchronize


        /// <summary>
        /// Send the specified packet.
        /// </summary>
        /// <param name='packet'>
        /// Packet.
        /// </param>
        /// <exception cref='ArgumentNullException'>
        /// Is thrown when an argument passed to a method is invalid because it is <see langword="null" /> .
        /// </exception>
        /// <exception cref='NotImplementedException'>
        /// Is thrown when a requested operation is not implemented for a given type.
        /// </exception>
        public void Send(Packet packet)
        {
            if (packet == null) 
                throw new ArgumentNullException("packet");

            if(packet.FromRemote)
                packet.FromRemote = false;
			
			if(!IsSynchronized)
				Synchronize();
			
			lock(_timeout)
			{
				// Reset timeout
				_timeout.Stop();
				_timeout.Start();
				
			} // lock(this)
			
            var buffer = new PacketBuffer(packet);
						
			_listener.Send(buffer.Data, 
               (int)buffer.Length, _endpoint);
            
			// Raise event for data sent
			OnDataSent(new DataEventArgs(packet));
			
        } // Send
		
		
		/// <summary>
		/// Receives the callback.
		/// </summary>
		/// <param name='result'>
		/// Result.
		/// </param>
		private void ReceiveCallback(IAsyncResult result)
        {
            // Check the connection state
            if (!IsConnected || _listener == null) 
				return;

            try
            {
                var endpoint = new IPEndPoint(0, 0);
                var data = _listener.EndReceive(
                    result, ref endpoint);
				
				lock(_timeout)
				{
					// Reset timeout
					_timeout.Stop();
					_timeout.Start();
					
				} // lock(_timeout)
				
                var packet = Packet
					.ParsePacket(data);
				
				if(packet is PunchingPacket)
				{
					// Save the remote endpoint
					if(!endpoint.Equals(_endpoint))
						_endpoint = endpoint;
					
					// Clients are synchronized
					_synchronized = true;
					
				} // if(packet is PunchingPacket)
				else if(packet is BroadcastPacket)
				{
					var broadcastPacket = packet 
						as BroadcastPacket;
					
					if(broadcastPacket.ClientId == Session.Id)
					{
						// Save the remote endpoint
						if(!endpoint.Equals(_endpoint))
							_endpoint = endpoint;
						
						// Save new address
						_address = endpoint.Address;
						
						// It is local network
						_localNetwork = true;
						
						// Clients are synchronized
						_synchronized = true;
						
					} // if(broadcastPacket.ClientId == Session.Id)
				} // else if(packet is BroadcastPacket)
				else
				{
					// Raise event for data received
					OnDataReceived(new DataEventArgs(packet));
					
				} // else
            } // try
            catch (Exception)
            {
            } // catch (Exception)
			
			lock(_timeout)
			{
				if (_listener != null)
	                _listener.BeginReceive(
	                    ReceiveCallback, null);
				
			} // lock(_timeout)
        } // ReceiveCallback

		
		
		/// <summary>
		/// Synchronizes the callback.
		/// </summary>
		/// <returns>
		/// The callback.
		/// </returns>
		/// <param name='port'>
		/// Port.
		/// </param>
		public int SynchronizeCallback(int port)
		{
			// Save the local port
			var local = Listen();
			
			Task.Factory.StartNew(()=>
            {
				if(_address.Equals(Session.Address))
				{
					var endpoint = new IPEndPoint(
						IPAddress.Broadcast, port);
					
					var buffer = new PacketBuffer(
						new BroadcastPacket(ClientId));
					
					for(int i = 0; i < 10; i++)
						_listener.Send(buffer.Data, 
		               		(int)buffer.Length, endpoint);
					
				} // if(_address.Equals(Session.Address))
				else
				{
					var endpoint = new IPEndPoint(
						_address, port);
					
					var buffer = new PacketBuffer(
						new PunchingPacket());
					
					for(int i = 0; i < 10; i++)
						_listener.Send(buffer.Data, 
		               		(int)buffer.Length, endpoint);
					
				} // else
			});
			
			return local;

		} // SynchronizeCallback


        /// <summary>
        /// Returns a <see cref="System.String"/> that represents the current <see cref="Lypshare.Net.Client.Connection"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents the current <see cref="Lypshare.Net.Client.Connection"/>.
        /// </returns>
        public override string ToString()
        {
            return _address + "/" + _clientId;

        } // ToString


        /// <summary>
        /// Releases all resource used by the <see cref="Lypshare.Net.Client.Connection"/> object.
        /// </summary>
        /// <remarks>
        /// Call <see cref="Dispose"/> when you are finished using the <see cref="Lypshare.Net.Client.Connection"/>.
        /// The <see cref="Dispose"/> method leaves the <see cref="Lypshare.Net.Client.Connection"/> in an unusable
        /// state. After calling <see cref="Dispose"/>, you must release all references to the
        /// <see cref="Lypshare.Net.Client.Connection"/> so the garbage collector can reclaim the memory that the
        /// <see cref="Lypshare.Net.Client.Connection"/> was occupying.
        /// </remarks>
        private void Dispose()
        {
            _connected = false;
			
			// Stop timeout
			_timeout.Stop();
			
			// Unlink disconnect
			_session.ClientLeft -= 
				OnSessionClientLeft;

            if (Tag != null)
                Tag.Dispose();
			
			if(_listener != null)
				_listener.Close();
			
			_listener = null;
			_session = null;
			_address = null;

            OnClosed();

        } // Dispose


        // --------------------------------------------------------
        // Class Handles
        // --------------------------------------------------------

		
		/// <summary>
		/// Raises the session client left event.
		/// </summary>
		/// <param name='arg1'>
		/// Arg1.
		/// </param>
		/// <param name='arg2'>
		/// Arg2.
		/// </param>
        private void OnSessionClientLeft(INetworkSession arg1, ConnectionEventArgs arg2)
        {
			if(arg2.Connection == this)
        		Dispose();
			
        } // OnSessionClientLeft
		
			
		/// <summary>
		/// Raises the timeout elapsed event.
		/// </summary>
		/// <param name='sender'>
		/// Sender.
		/// </param>
		/// <param name='e'>
		/// E.
		/// </param>
        private void OnTimeoutElapsed(object sender, ElapsedEventArgs e)
        {
        	lock(_timeout)
			{
				lock(this)
				{
					_synchronized = false;
					
					if(_listener != null)
						_listener.Close();
					
					_listener = null;
					
				} // lock(this)
			} // lock(_timeout)
        } // OnTimeoutElapsed
		

        /// <summary>
        /// Raises the data received event.
        /// </summary>
        /// <param name='e'>
        /// E.
        /// </param>
        protected virtual void OnDataReceived(DataEventArgs e)
        {
            if (DataReceived != null)
                DataReceived(this, e);

        } // OnDataReceived


        /// <summary>
        /// Raises the data sent event.
        /// </summary>
        /// <param name='e'>
        /// E.
        /// </param>
        protected virtual void OnDataSent(DataEventArgs e)
        {
            if (DataSent != null)
                DataSent(this, e);

        } // OnDataSent


        /// <summary>
        /// Raises the closed event.
        /// </summary>
        protected virtual void OnClosed()
        {
            if (Closed != null)
                Closed(this);

        } // OnClosed

    } // class Connection
} // namespace Lypshare.Net.Client